# 要添加一个新单元，输入 '# %%'
# 要添加一个新的标记单元，输入 '# %% [markdown]'
#%%
import torch
import torch.nn as nn
# %% [markdown]
# ## 乳腺癌的预测
# %% [markdown]
# ---
# %% [markdown]
# #### 介绍
# %% [markdown]
# 上一个试验我们讲解了线性问题的求解步骤，本实验我们以乳腺癌的预测为实例，详细的阐述如何利用 PyTorch 求解一个非线性问题。
# %% [markdown]
# #### 知识点
# %% [markdown]
# - 数据集的标准化
# - 数据集的划分
# - Sigmoid 函数
# - 乳腺癌的预测
# %% [markdown]
# ---
# %% [markdown]
# ### 数据集的预处理
# %% [markdown]
# #### 数据集的加载
# %% [markdown]
# 首先，让我们来加载数据集合。这里我们使用 `pandas` 对数据集合进行加载：

# %%
import pandas as pd
df = pd.read_csv(
    'https://labfile.oss.aliyuncs.com/courses/2534/breast_cancer.csv', index_col=False)
df

# %%
import pandas as pd
# pandas可以读取网络上的数据
df = pd.read_csv(r'https://labfile.oss.aliyuncs.com/courses/2534/breast_cancer.csv')
df


# %%
print(df.describe())
# 尝试分析数据
import seaborn as sns
sns.set()
# 一共有31行，最后一行是目标值01是否患乳腺癌

# %% [markdown]
# 可以看到该数据集合一共有 569 条数据，每条数据有 30 个和乳腺癌相关的病变特征，最后一列是该患者是否患有乳腺癌的诊断结果。其中 0 表示没有患有乳腺癌，1 表示患有乳腺癌。
# %% [markdown]
# 我们可以利用 pandas 中的切片，先将上表中的特征和标签分开：

# %%
# 先把特征和标签分开,也就是把最后一列和前面的列分开，分别作为X和Y
X = df[df.columns[0:-1]].to_numpy()
y = df[df.columns[-1]].to_numpy()
X.shape, y.shape


# %%
df.columns
# 返回df的所有的列的标签
X = df[df.columns[0:-1]].to_numpy()
y = df[df.columns[-1]].to_numpy()
# 然后按标签进行切片（这就是对列进行切片了），然后再按标签进行索引，就完成了

# 先转换成numppy再切片也可以
# 但是要注意维度，是对列进行切片在第1维度上，还是先切片再转换成numpy好
X = df.to_numpy()[:,:-1]
y = df.to_numpy()[:,-1]
X.shape, y.shape

# %% [markdown]
# 可以看到共有 569 条数据，每条数据有 30 个特征和 1 个标签。
# %% [markdown]
# #### 数据集的划分和标准化
# %% [markdown]
# 为了能够评价模型的好坏，这里我们利用 `sklearn.model_selection` 函数，将原数据按比例随机分为训练数据集和测试数据集，如下：

# %%
from sklearn.model_selection import train_test_split
# 按照 0.8 和 0.2 的比例随机划分数据集合
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=1234)
X_train.shape, y_train.shape, X_test.shape, y_test.shape


# %%
# 使用sklearn来进行数据的切分
from sklearn.model_selection import train_test_split
# 按照0.8比0.2的比例进行随机划分数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1234)
# 注意这个train_test_split是以train test train test的方式返回的
X_train.shape,y_train.shape,X_test.shape,y_test.shape

# %% [markdown]
# 为了加快模型的收敛速度，一般我们都需要对原始数据进行标准化处理，将所有的数据按照比例缩放到一定范围内。
# %% [markdown]
# 这里我们可以使用 `sklearn.preprocessing` 来对数据集合进行标准化。

# %%
# 进行标准化处理
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
# 对特征进行标准化，标签不要标准化，因为标签只有 0 和 1
# 对训练集要fit_transform
X_train = sc.fit_transform(X_train)
# 对测试集只进行transform
X_test = sc.transform(X_test)
X_train


# %%
# 进行标准化处理
from sklearn.preprocessing import StandardScaler
# StandardScaler()
# z = (x - u) / s
# 先实例化standardscaler
sc = StandardScaler()
# 一般来说对训练数据进行fit_transform，对测试数据进行transform
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)
X_train

# %% [markdown]
# 最后，为了将数据放入 PyTorch 定义的模型之中，我们必须将所有的数据转为 张量类型：

# %%
import torch
import numpy as np

X_train = torch.from_numpy(X_train.astype(np.float32))
X_test = torch.from_numpy(X_test.astype(np.float32))
y_train = torch.from_numpy(y_train.astype(np.float32))
y_test = torch.from_numpy(y_test.astype(np.float32))

# 将标签也转为 2 维，否则放入模型之中训练时，可能出错
y_train = y_train.view(y_train.shape[0], 1)
y_test = y_test.view(y_test.shape[0], 1)
X_train.size(), y_train.size()


# %%
# 转换为tensor
# 先把numpy转换为32位float然后再转换成tensor
import torch

X_train = torch.from_numpy(X_train.astype(np.float32))
X_test = torch.from_numpy(X_test.astype(np.float32))
# 标签也要转换，而且还要reshap成二维的
# 这里是先reshape到二维，再转换成tensor
y_train = torch.from_numpy(y_train.astype(np.float32).reshape(-1,1))
y_test = torch.from_numpy(y_test.astype(np.float32).reshape(-1,1))


# %% [markdown]
# ### 乳腺癌的预测
# %% [markdown]
# #### 模型的定义
# %% [markdown]
# 在处理完数据后，接下来，我们就需要建立相应的模型，用于乳腺癌的预测了。
# %% [markdown]
# 线性函数是一条没有上界和下界的直线，即线性函数预测出来的值可以很大如 112321442，也可以很小如 -1231242412。而本实验的数据标签只有 0（患病） 或 1（不患病），因此用线性函数来拟合乳腺癌的数据点是不合理的。
# %% [markdown]
# 我们需要找到输出始终为 0-1 之间的函数模型。如果拥有这样的函数模型，那么将任意 x 放入该模型中，都会输出一个 0-1 之间的值。这个值我们可以看做是患有乳腺癌的概率。如果这个概率值小于 0.5 则表示没有患乳腺癌。如果这个概率值大于 0.5 则表示患有乳腺癌。
# %% [markdown]
# 逻辑回归函数 Sigmoid 就是这样一种函数，该函数又叫做激活函数，公式如下： 
# %% [markdown]
# $$\sigma = \frac{1}{1+e^{-z}}$$
# %% [markdown]
# 该函数的几何形式如下所示：
# %% [markdown]
# <img width="400px" src="https://doc.shiyanlou.com/courses/2534/1166617/d3267547f64d893dfb1fb73ecd80a879-0">
# %% [markdown]
# 从图中我们可以看出，该函数就是一个上下界分别为 1 和 0  的有界非线性函数。
# %% [markdown]
# 我们可以让通过了线性函数的输出，再通过一次上面的激活函数，进而得到 0-1 之间的结果。
# %% [markdown]
# 综上，乳腺癌的预测模型如下：

# %%
import torch.nn as nn
# 我们的模型是一个线性函数+激活函数的非线性模型
# modle(x) = sigmoid(w*x+b)

# 继承至nn.Module
class Model(nn.Module):
    # 初始化需要的模型层
    def __init__(self, n_input_features):
        super(Model, self).__init__()
        self.linear = nn.Linear(n_input_features, 1)
    # 编写前向传播
    def forward(self, x):
        # torch 中已经定义了 sigmoid 函数模型
        y_pred = torch.sigmoid(self.linear(x))
        return y_pred


# 获得样本量和特征数
n_samples, n_features = X.shape
# 模型的初始化
model = Model(n_features)
model


# %%
class model(nn.Model):

# %% [markdown]
# 至此，我们就得到了一个乳腺癌的初始模型。由于最后通过了一层逻辑回归函数，无论输入的值为多少，模型的输出都必定属于 0-1 之间。
# %% [markdown]
# #### 损失函数和优化器
# %% [markdown]
# 接下来的步骤和上个实验中的步骤类似。
# %% [markdown]
# 首先，让我们来定义一下损失函数，由于我们的标签只有 0 和 1，因此这里使用二元交叉熵损失来计算真实值和预测值之间的距离了。该损失函数的公式如下：
# %% [markdown]
# $$L = -\sum_{i=1}^N y^i log \hat{y}^i + (1-y^i)log(1-\hat{y}^i)$$
# %% [markdown]
# 当然，我们不必手写上面的损失函数， 直接使用  `nn.BCELoss()` 即可：

# %%
# 损失和优化器的定义
# 迭代次数
num_epochs = 100
# 学习率
learning_rate = 0.01
# 二元交叉熵损失
criterion = nn.BCELoss()
# SGD 优化器
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
criterion, optimizer


# %%


# %% [markdown]
# #### 模型的训练
# %% [markdown]
#  定义完损失函数和优化器后，接下来的模型训练步骤就是固定的了，如下：
# %% [markdown]
# - 通过模型的正向传播，输出预测结果。
# - 通过预测结果和真实标签计算损失。
# - 通过后向传播，获得梯度。
# - 通过梯度更新模型的权重。
# - 进行梯度的清空。
# - 循环上面操作，直到损失较小为止。
# %% [markdown]
# 让我们用代码完成上面的步骤：

# %%
for epoch in range(num_epochs):
    y_pred = model(X_train)
    loss = criterion(y_pred, y_train)
    # 后向传播、梯度更新、梯度清空
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

    if (epoch+1) % 10 == 0:
        print(f'epoch: {epoch+1}, loss = {loss.item():.4f}')
print("模型训练完毕！！")


# %%


# %% [markdown]
# 综上，我们训练好了一个乳腺癌的预测模型。我们可以尝试对任意一条数据进行预测：

# %%
index = np.random.randint(0, len(X_test))
y_predicted = model(X_test[index])
# 小于 0.5 则输出 0 ，大于0.5 则输出 1
y_predicted_cls = y_predicted.round()

# 将结果转为 numpy类型
real = y_test[index].detach().numpy()[0]
predict = y_predicted_cls.detach().numpy()[0]
print("第 {} 条测试数据的真实结果为 {} ，预测结果为 {} "
      .format(index, real, predict))


# %%


# %% [markdown]
# 由于模型准确率不是 100%，因此，上面的预测结果和真实结果也可能会不相同。但是，你多运行几次上面代码，必定会出现预测结果和真实结果相同的情况。 
# %% [markdown]
# 那么，我们训练出来的模型准确率到底是多少呢？

# %%
with torch.no_grad():
    y_predicted = model(X_test)
    y_predicted_cls = y_predicted.round()
    acc = y_predicted_cls.eq(y_test).sum().numpy() / float(y_test.shape[0])
    print(f'accuracy: {acc.item():.4f}')


# %%


# %% [markdown]
# 我们利用测试数据，计算出了整个模型的预测准确率大概在 90% 左右，证明我们的模型可以很好地进行乳腺癌的诊断预测。
# %% [markdown]
# ### 实验总结
# %% [markdown]
# 本实验以乳腺癌的预测为例，引入了激活函数 sigmoid 的概念。建立了一个简单的非线性模型用于诊断患者是否患有乳腺癌。其实，本实验建立的一个线性函数+激活函数的模型就是一个简单的神经网络模型。全连接神经网络的实质其实就是无数个线性函数和非线性网络组成的集合。
# %% [markdown]
# <hr><div style="color: #999; font-size: 12px;"><i class="fa fa-copyright" aria-hidden="true"> 本课程内容版权归蓝桥云课所有，禁止转载、下载及非法传播。</i></div>

